package ru.averta.notes.service.math;

import java.util.LinkedList;
import java.util.List;

/**
 * <code>DameauLevenshteinDistanceResult</code>
 *
 * @author fedor.belov (fedor.belov@mail.ru)
 *         Date: 06.09.11 14:39
 */
public class DistanceResult {

    //Distance matrix
    private int[][] distanceMatrix;

    //Матрица изменений
    private byte[][] changeMatrix;

    //Исходное слово
    private char[] sourceArray;

    //Слово из словаря
    private char[] targetArray;

    //Небольшая оптимизация производительности
    private int prefixDistance = -1;
    private int prefixDistanceIndex = -1;

    //Значение определяет допустимость преобразования по формуле (длина_исх_слова/фактор) > (префиксное расстояние)
    byte prefixDistanceFactor;

    /**
     * Instantiate the object with the resulting prefixDistance matrix
     *
     * @param distanceMatrix Matrix of distances between edits
     */
    public DistanceResult(char[] sourceArray, char[] targetArray, int[][] distanceMatrix, byte[][] changeMatrix, byte prefixDistanceFactor) {
        this.sourceArray = sourceArray;
        this.targetArray = targetArray;
        this.distanceMatrix = distanceMatrix;
        this.changeMatrix = changeMatrix;
        this.prefixDistanceFactor = prefixDistanceFactor;
    }

    /**
     * Get the Distance Matrix
     *
     * @return Matrix of edit distances
     */
    public int[][] getDistanceMatrix() {
        return distanceMatrix;
    }

    public String getResultString() {
        return String.valueOf(targetArray);
    }

    public String getSameLettersBit() {
        char[] sameLettersBit = new char[targetArray.length];
        List<Integer> saved = getSameLetters();

        for (int j = 1; j<=targetArray.length; j++) {
            sameLettersBit[j-1] = (saved.contains(j-1)) ? '0' : '1';
        }

        return String.valueOf(sameLettersBit);
    }

    /**
     * Возвращает список букв, которые были изменены в новом слове
     * Непонятно, что делать с удаленными буквами?
     * На основании http://www.adotof.ru/?p=161 (раздел "Алгоритм Вагнера — Фишера")
     *
     * @return
     */
    public List<Integer> getChangedLetters() {
        List<Integer> changed = new LinkedList<Integer>();

        int i = changeMatrix.length - 1;
        int j = changeMatrix[0].length - 1;

        while (i != 0 || j != 0) {
            if (changeMatrix[i][j] == EditDistance.FLAG_INSERTED) {
                changed.add(j);
                j--;
            } else if (changeMatrix[i][j] == EditDistance.FLAG_CHANGED) {
                changed.add(j);
                i--;
                j--;
            } else if (changeMatrix[i][j] == EditDistance.FLAG_SAME) {
                i--;
                j--;
            } else if (changeMatrix[i][j] == EditDistance.FLAG_DELETED) {
                i--; //TODO подумать, что делать с удаленными буквами
            } else if (changeMatrix[i][j] == EditDistance.FLAG_EXCHANGED) {
                changed.add(j);
                changed.add(j-1);
                i = i - 2;
                j = j - 2;
            }
        }

        return changed;
    }

    public String getChangeBit() {
        char[] changeBit = new char[targetArray.length];
        List<Integer> changed = getChangedLetters();

        for (int j = 1; j<=targetArray.length; j++) {
            changeBit[j-1] = (changed.contains(j)) ? '1' : '0';
        }

        return String.valueOf(changeBit);
    }

    public List<Integer> getSameLetters() {
        List<Integer> saved = new LinkedList<Integer>();

        int i = distanceMatrix.length - 1;
        int j = getPrefixDistanceIndex();

        while (i > 0 && j > 0) {
            if (distanceMatrix[i][j] == distanceMatrix[i - 1][j - 1] && changeMatrix[i][j] != EditDistance.FLAG_EXCHANGED) {
                saved.add(j - 1);
            }

            i--;
            j--;
        }

        return saved;
    }

    /**
     * Возвращает true, если преобразование допустимо
     *
     * @return
     */
    public boolean isSufficientPrefixDistance() {
        return (Math.min(sourceArray.length, targetArray.length) > getPrefixDistance()*(int)prefixDistanceFactor);
    }

    public boolean isSufficientDistance() {
        return (Math.min(sourceArray.length, targetArray.length) > getDistance()*(int)prefixDistanceFactor);
    }

    /**
     * Get the Edit Distance
     *
     * @return number of changes to make before
     *         both strings are identical
     */
    public int getPrefixDistance() {
        if (prefixDistance == -1) loadPrefixDinstanceAndDinstanceIndex();
        return prefixDistance;
    }

    public int getDistance() {
        return distanceMatrix[sourceArray.length][targetArray.length];
    }

    /**
     * Возвращает позицию в матрице (j), значение которой отображает минимальную стоимость преобразования
     * исходного слова к конечному
     *
     * @return
     */
    public int getPrefixDistanceIndex() {
        if (prefixDistanceIndex == -1) loadPrefixDinstanceAndDinstanceIndex();
        return prefixDistanceIndex;
    }

    /**
     * Вычисляет значения prefixDistance и prefixDistanceIndex
     */
    private void loadPrefixDinstanceAndDinstanceIndex() {
        // Возьмем минимальное префиксное расстояние
        prefixDistance = distanceMatrix[sourceArray.length][0];
        for (int i = 1; i <= targetArray.length; ++i) {
            if (distanceMatrix[sourceArray.length][i] < prefixDistance) {
                prefixDistance = distanceMatrix[sourceArray.length][i];
                prefixDistanceIndex = i;
            }
        }
    }

    /**
     * Get a string representation of this class
     *
     * @return A friendly display of the prefixDistance and matrix
     */
    @Override
    public String toString() {
        List<Integer> sameLetters = getSameLetters();
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Distance: %s \n", this.getPrefixDistance()));
        sb.append("Сохраненные буквы: ");
        for (int j = 0; j < targetArray.length; j ++) {
            if (sameLetters.contains(j)) {
                sb.append(targetArray[j]); }
            else {
                sb.append("_");
            }
        }
        sb.append("\n");
        sb.append("Matrix: \n\n");

        for (int i = 0; i < this.distanceMatrix.length; i++) {

            if (i == 0) {
                //Формируем заголовок
                sb.append("  | ");
                for (int j = 0; j < this.distanceMatrix[0].length; j++) {
                    if (j == 0) {
                        sb.append(String.format("\t%s", " "));
                    } else {
                        sb.append(String.format("\t%s", targetArray[j - 1]));
                    }

                }
                sb.append("\n");
            }

            if (i == 0) {
                sb.append(" ");
            } else {
                sb.append(sourceArray[i-1]);
            }

            sb.append(" | ");
            for (int j = 0; j < this.distanceMatrix[0].length; j++) {
                sb.append(String.format("\t%s", this.distanceMatrix[i][j]));
            }
            sb.append("\n");
        }

        return sb.toString();
    }
}
